iT邦幫忙

2026 iThome 鐵人賽

DAY 28
0
Modern Web

現在就學C# 與 ASP.NET Core系列 第 28 篇

Day 28|Dependency Injection

  • 分享至 

  • xImage
  •  

Day 27 我們完成了 Product API,但目前 ProductsController 同時負責:

  • 處理 HTTP Request / Response
  • 查詢、新增、修改與刪除 Product

當專案越來越大,Controller 容易累積太多程式邏輯。

因此,我們希望把責任分開:

ProductsController
→ 處理 HTTP Request / Response

ProductService
→ 處理 Product 相關操作

但這時會出現一個問題:

ProductsController 需要 ProductService,那 ProductService 應該由誰建立?

最直覺的方式可能是:

private readonly ProductService _productService =
    new ProductService();

但這代表 Controller 不只要「使用」Service,還要負責「建立」Service。

ASP.NET Core 提供了另一種方式:

Dependency Injection
DI
依賴注入

今天就從目前的 Product API 出發,理解 DI 如何解決這個問題。


1. 什麼是 Dependency Injection?

假設:

ProductsController
        ↓
需要 ProductService

ProductService 就是 ProductsController 的:

Dependency
→ 依賴

如果直接:

ProductService productService =
    new ProductService();

代表 Class 自己負責建立 Dependency。

而 Dependency Injection 的做法是:

Class 不自己建立需要的 Dependency,而是由外部提供。

兩種方式可以這樣比較:

自己 new Dependency Injection
Class 自己建立 Dependency Dependency 由外部提供
使用與建立綁在一起 使用與建立分開
Dependency 較難替換 Dependency 較容易替換
自己管理生命週期 可交由 DI Container 管理

所以今天先建立一個最重要的概念:

自己 new
→ 自己建立、自己使用

DI
→ 外部建立、Class 負責使用

ASP.NET Core 本身就有內建的 DI 機制,可以幫我們管理這些 Service。


2. 調整 Product API 專案結構

今天要把原本 Controller 裡的 Product 操作抽到:

ProductService

專案結構調整成:

MyApi2
│
├─ Controllers
│  └─ ProductsController.cs
│
├─ Models
│  ├─ Product.cs
│  ├─ CreateProductRequest.cs
│  └─ UpdateProductRequest.cs
│
├─ Services
│  ├─ IProductService.cs
│  └─ ProductService.cs
│
└─ Program.cs

各自負責:

檔案 負責什麼?
IProductService.cs 定義 Product Service 的功能
ProductService.cs 實作 Product 相關操作
ProductsController.cs 處理 HTTP Request / Response
Program.cs 註冊 Service

Models 繼續沿用 Day 27,不需要重新建立。


3. 建立 IProductService.cs

建立:

Services/IProductService.cs

完整程式碼:

using MyApi2.Models;

namespace MyApi2.Services;

public interface IProductService
{
    IEnumerable<Product> GetAll();

    Product? GetById(int id);

    Product Create(CreateProductRequest request);

    bool Update(
        int id,
        UpdateProductRequest request
    );

    bool Delete(int id);
}

IProductService 是 Interface。

它負責定義:

Product Service 可以提供哪些操作。

但不負責真正處理資料。

可以先理解成:

IProductService
→ 定義要做什麼

ProductService
→ 負責實際怎麼做

補充一點:

DI 並不一定需要搭配 Interface。

今天使用 IProductService,是讓 Controller 依賴抽象,也方便之後替換實作與測試。


4. 建立 ProductService.cs

建立:

Services/ProductService.cs

完整程式碼:

using MyApi2.Models;

namespace MyApi2.Services;

public class ProductService : IProductService
{
    private static readonly List<Product> Products =
    [
        new Product(1, "Mouse", 1000m)
    ];

    private static int nextId = 2;

    public IEnumerable<Product> GetAll()
    {
        return Products;
    }

    public Product? GetById(int id)
    {
        return Products.FirstOrDefault(
            product => product.Id == id
        );
    }

    public Product Create(
        CreateProductRequest request
    )
    {
        Product product = new(
            nextId,
            request.Name,
            request.Price
        );

        nextId++;
        Products.Add(product);

        return product;
    }

    public bool Update(
        int id,
        UpdateProductRequest request
    )
    {
        int index = Products.FindIndex(
            product => product.Id == id
        );

        if (index == -1)
        {
            return false;
        }

        Products[index] = new Product(
            id,
            request.Name,
            request.Price
        );

        return true;
    }

    public bool Delete(int id)
    {
        Product? product =
            Products.FirstOrDefault(
                product => product.Id == id
            );

        if (product is null)
        {
            return false;
        }

        Products.Remove(product);

        return true;
    }
}

現在原本 Controller 裡的:

Products
nextId
查詢
新增
修改
刪除

都移到了 ProductService。

責任開始分開:

ProductsController
→ HTTP Request / Response

ProductService
→ Product 操作

目前的:

static List<Product>

仍然只是模擬 Database。

之後學習 EF Core 時,這裡會改成真正的資料庫操作。


5. Program.cs:註冊 Service

現在已經有:

IProductService
ProductService

接下來還要告訴 ASP.NET Core:

當程式需要 IProductService 時,要提供 ProductService。

修改:

Program.cs

完整程式碼:

using MyApi2.Services;

var builder =
    WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

builder.Services.AddScoped<
    IProductService,
    ProductService
>();

var app = builder.Build();

app.UseHttpsRedirection();

app.MapControllers();

app.Run();

今天最重要的是:

builder.Services.AddScoped<
    IProductService,
    ProductService
>();

可以先直接讀成:

需要 IProductService
↓
提供 ProductService

其中:

builder.Services
→ 註冊 Service

IProductService
→ 要使用的 Service Type

ProductService
→ 實際提供的實作

AddScoped
→ 設定 Service Lifetime

這一步稱為:

註冊 Service

6. 修改 ProductsController.cs

接著修改:

Controllers/ProductsController.cs

完整程式碼:

using Microsoft.AspNetCore.Mvc;
using MyApi2.Models;
using MyApi2.Services;

namespace MyApi2.Controllers;

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IProductService _productService;

    public ProductsController(
        IProductService productService
    )
    {
        _productService = productService;
    }

    [HttpGet]
    public ActionResult<IEnumerable<Product>>
        GetAll()
    {
        IEnumerable<Product> products =
            _productService.GetAll();

        return Ok(products);
    }

    [HttpGet("{id:int}")]
    public ActionResult<Product> GetById(int id)
    {
        Product? product =
            _productService.GetById(id);

        if (product is null)
        {
            return NotFound();
        }

        return Ok(product);
    }

    [HttpPost]
    public ActionResult<Product> Create(
        CreateProductRequest request
    )
    {
        Product product =
            _productService.Create(request);

        return CreatedAtAction(
            nameof(GetById),
            new { id = product.Id },
            product
        );
    }

    [HttpPut("{id:int}")]
    public IActionResult Update(
        int id,
        UpdateProductRequest request
    )
    {
        bool updated =
            _productService.Update(
                id,
                request
            );

        if (!updated)
        {
            return NotFound();
        }

        return NoContent();
    }

    [HttpDelete("{id:int}")]
    public IActionResult Delete(int id)
    {
        bool deleted =
            _productService.Delete(id);

        if (!deleted)
        {
            return NotFound();
        }

        return NoContent();
    }
}

跟 Day 27 比較,Controller 已經不再直接:

操作 List<Product>
產生 nextId
新增 Product
修改 Product
刪除 Product

而是改成呼叫:

_productService.GetAll();
_productService.GetById(id);
_productService.Create(request);
_productService.Update(id, request);
_productService.Delete(id);

Controller 現在比較專注在:

接收 HTTP Request
↓
呼叫 Service
↓
根據結果回傳 HTTP Response

例如:

Product? product =
    _productService.GetById(id);

if (product is null)
{
    return NotFound();
}

return Ok(product);

Controller 負責決定:

找到 Product
→ 200 OK

找不到 Product
→ 404 Not Found

至於 Product 實際怎麼取得,則交給 ProductService。


7. Constructor Injection 是怎麼運作的?

現在看整篇最重要的一段:

private readonly IProductService _productService;

public ProductsController(
    IProductService productService
)
{
    _productService = productService;
}

這種方式稱為:

Constructor Injection
→ 建構子注入

Constructor:

public ProductsController(
    IProductService productService
)

可以直接理解成:

建立 ProductsController 時,我需要一個 IProductService。

因為前面已經在 Program.cs 建立:

IProductService
→ ProductService

的註冊關係,所以 ASP.NET Core 可以找到對應的 ProductService,並提供給 Controller。

Controller 再把收到的 Dependency 保存:

_productService = productService;

之後 Action 就可以直接使用:

_productService.GetById(id);

整個過程:

Controller
宣告需要 IProductService
↓
DI Container
找到 ProductService
↓
提供給 Constructor
↓
Controller 使用 Service

Controller 不需要自己:

new ProductService();

8. Service Lifetime

前面註冊 Service 時使用:

builder.Services.AddScoped<
    IProductService,
    ProductService
>();

其中:

Scoped

代表 Service 的:

Lifetime
→ 生命週期

ASP.NET Core 常見三種:

Lifetime 基本概念
Transient 每次向 Container 解析時建立新的 Instance
Scoped 同一個 Request Scope 共用相同 Instance
Singleton Application 共用相同 Instance

今天使用:

AddScoped

在一般 Web Request 中,可以先理解成:

Request A
→ 一個 ProductService Instance

Request B
→ 另一個 ProductService Instance

同一個 Request Scope 中取得同一個 Scoped Service,會使用相同的 Instance。

目前先理解三種 Lifetime 的基本差異即可,不需要深入所有使用情境。

後面學習 EF Core 時,還會再次看到 Scoped。


9. 一次看懂 DI:註冊 → 注入 → 使用

現在回到最初的問題:

ProductsController 需要 ProductService,那 ProductService 到底從哪裡來?

其實只需要看三個位置。

① 註冊

Program.cs

builder.Services.AddScoped<
    IProductService,
    ProductService
>();

建立:

IProductService
→ ProductService

的註冊關係。


② 注入

ProductsController.cs

public ProductsController(
    IProductService productService
)
{
    _productService = productService;
}

Controller 宣告需要:

IProductService

由 DI Container 提供。


③ 使用

Controller Action:

Product? product =
    _productService.GetById(id);

直接使用已經注入的 Service。

所以整個 DI 流程可以濃縮成:

Program.cs
註冊
IProductService → ProductService
        ↓
ProductsController
Constructor Injection
        ↓
_productService
        ↓
Controller Action
使用 Service

也就是今天最重要的三個步驟:

註冊 → 注入 → 使用


Day 28 小結

Day 27 的 Controller:

ProductsController
→ HTTP
→ Product 操作

Day 28 把責任拆開:

ProductsController
→ HTTP Request / Response

ProductService
→ Product 操作

再透過 DI 把兩者連接:

Program.cs
→ 註冊

Constructor
→ 注入

Controller Action
→ 使用

Dependency Injection 的核心,是讓 Class 專心使用 Dependency,把 Dependency 的建立與管理交給外部。


上一篇
Day 27|Model Binding、DTO 與 Validation
下一篇
Day 29|Middleware 與 Global Exception Handling
系列文
現在就學C# 與 ASP.NET Core 共 29 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言